Skip to content

Static/dynamic config controls - #750

Merged
theoephraim merged 21 commits into
mainfrom
codex/static-dynamic-integration
Jul 28, 2026
Merged

Static/dynamic config controls#750
theoephraim merged 21 commits into
mainfrom
codex/static-dynamic-integration

Conversation

@theoephraim

@theoephraim theoephraim commented Jun 5, 2026

Copy link
Copy Markdown
Member

Adds first-class control over whether config items are static (eligible for build-time inlining) or dynamic (must stay runtime-resolved), decoupled from sensitivity. Previously the only way to keep a value out of build-time bundles was to mark it @sensitive.

Schema

  • @dynamic / @static item decorators (boolean-valued, mutually exclusive)
  • @defaultDynamic root decorator: true, false, or inferFromSensitive
  • Default is inferFromSensitive: dynamic follows sensitivity, so existing schemas behave exactly as before. The serialized blob only carries isDynamic when it diverges from isSensitive; consumers read isDynamic ?? isSensitive.

Runtime + integrations

  • All bundler inlining (vite, next webpack/turbopack, expo babel) now gates on isDynamic instead of isSensitive. Secrecy behavior (redaction, leak scanning, sourcemap scrubbing, wrangler secrets) stays on isSensitive. Expo warns at build time when any non-inlined (sensitive or dynamic) key is referenced from native code.

  • Public+dynamic values load in the browser via loadPublicDynamicEnv() hydrating the same ENV.KEY proxy, served from a JSON endpoint (getPublicDynamicEnv()). Astro auto-injects /__varlock/public-env when public+dynamic keys exist (configurable via publicDynamicEndpoint); Next/SvelteKit add a one-liner route. Hydration is filtered to declared public+dynamic keys; every integration (including Next, via the webpack client runtime chunk / turbopack client components) injects the declared key list (names only) into client bundles.

  • Next.js: accessing a public+dynamic key during server rendering marks the route dynamic (runtime access hook calling headers()), including nested component access. Works on both bundlers; a source-rewrite approach was dropped after breaking pages-router builds.

  • Vite-based builds set __VARLOCK_EXECUTION_PHASE=build; accessing a public+dynamic key during build/prerender errors (downgradable via _VARLOCK_DYNAMIC_BUILD_ACCESS_MODE=warn). Sensitive values are deliberately not guarded: reading them server-side during SSG is legitimate, and leak scanning polices the output.

  • Boot-time injection alternative to the endpoint: the browser runtime auto-hydrates from a globalThis.__varlockPublicDynamicEnv global if present, so a server/container entrypoint can extract the public+dynamic subset (varlock load --filter '@dynamic,!@sensitive' --format json) and inject it as a script tag into served HTML (recipe in the guide).

Filtering

  • New @dynamic filter selector (!@dynamic for static items) in --filter and codegen filter=.
  • Decorator-selector filters (@dynamic/@sensitive/@required) now scope resolution and validation via metadata-first resolution: item decorator metadata resolves first (no value resolvers run), the filter matches exactly, and only selected items plus deps get values resolved. So varlock load --filter='!@dynamic' at build time skips runtime-only vars entirely, including their @required checks. A broken value that the filter evaluation itself depended on still fails the load.

Validation

  • varlock unit suite, plus framework tests: Next.js v16 (webpack + turbopack, incl. pages router), Astro v5, SvelteKit, Vite (dynamic non-inlining + HTML replacement), expo
  • docs: new Static vs Dynamic guide (framework recipes centralized there in tabs, with boot-time-resolution and "when do you need this" framing), decorator/CLI/reserved-variable references, short per-integration sections

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown
Contributor

bumpy-frog

The changes in this PR will be included in the next version bump.

minor Minor releases

  • @varlock/astro-integration 1.2.1 → 1.3.0
  • @varlock/cloudflare-integration 1.3.2 → 1.4.0 (cascade)
  • @varlock/expo-integration 1.1.1 → 1.2.0
  • @varlock/nextjs-integration 1.1.6 → 1.2.0
  • @varlock/vite-integration 1.3.1 → 1.4.0
  • varlock 1.13.0 → 1.14.0

Bump files in this PR

Click here if you want to add another bump file to this PR


This comment is maintained by bumpy.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jun 5, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
varlock-website d9b7245 Commit Preview URL

Branch Preview URL
Jul 28 2026, 06:53 AM

@theoephraim theoephraim changed the title Static/dynamic config controls + dynamic public framework flows Static/dynamic config controls (wip) Jun 5, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

varlock

npm i https://pkg.pr.new/varlock@750

@varlock/astro-integration

npm i https://pkg.pr.new/@varlock/astro-integration@750

@varlock/cloudflare-integration

npm i https://pkg.pr.new/@varlock/cloudflare-integration@750

@varlock/expo-integration

npm i https://pkg.pr.new/@varlock/expo-integration@750

@varlock/nextjs-integration

npm i https://pkg.pr.new/@varlock/nextjs-integration@750

@varlock/vite-integration

npm i https://pkg.pr.new/@varlock/vite-integration@750

@varlock/1password-plugin

npm i https://pkg.pr.new/@varlock/1password-plugin@750

commit: d9b7245

…blic+dynamic keys, fix Next headers resolution under webpack externals, rename __VARLOCK_EXECUTION_PHASE (varlock-set convention), defaultDynamic=inferFromSensitive, consolidate publicDynamic API naming, harden hydration + proxy perf, astro endpoint fail-closed + explicit static-output opt-in, drop unneeded per-process cache-busting
…corator filters; register new reserved env vars; docs
@theoephraim
theoephraim force-pushed the codex/static-dynamic-integration branch from 239c7f8 to cbe9b52 Compare July 24, 2026 18:33
@theoephraim theoephraim changed the title Static/dynamic config controls (wip) Static/dynamic config controls Jul 24, 2026
@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

📦 Bundle size

⚠️ grows the bundle by 63.9 KB (+1.4%)

Metric main This PR Δ
Total dist 4727.1 KB 4791.1 KB +63.9 KB (+1.4%)
JS 1633.3 KB 1656.5 KB +23.2 KB (+1.4%)
Sourcemaps 3024.6 KB 3058.6 KB +34.0 KB (+1.1%)
Type defs 69.2 KB 76.0 KB +6.8 KB (+9.8%)

dist/ only; native binaries are versioned separately and not counted here.

@theoephraim
theoephraim marked this pull request as ready for review July 24, 2026 18:38
…ork tests for dynamic non-inlining + html error, CF blob type includes isDynamic, docs (Next client pre-hydration caveat, CF row, SSR freshness note)
…bpack runtime chunk + turbopack client components); client-bundle assertions in dynamic scenario
…oy; endpoint only useful for build-once-deploy-many)
…nferFromPrefix from recipe examples (explicit @public instead); fix guide defaults example
…rink integration pages to facts + pointers, clarify boot-time (not per-access) resolution
…arlockPublicDynamicEnv global (fixes auto-init TDZ by moving it to module bottom) + docker entrypoint recipe in guide
…under one section, boot-time injection steps, trim integration pages to minimal pointers
@theoephraim

Copy link
Copy Markdown
Member Author

@pullfrog review this PR

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues — one minor correctness gap noted inline.

Reviewed changes — first-class static/dynamic config controls, decoupling build-time inlining eligibility from @sensitive, across the schema layer, runtime, CLI filters, and every framework integration.

  • @dynamic/@static item decorators + @defaultDynamic root decorator — new processDynamic() in config-item.ts computes isDynamic (default linkage inferFromSensitive), serialized into the blob only when it diverges from isSensitive (consumers read isDynamic ?? isSensitive).
  • Inlining gates flipped from isSensitive to isDynamic — vite, next (webpack/turbopack loader + plugin), expo babel, and getBuildTimeReplacements now skip inlining based on isDynamic ?? isSensitive; secrecy behavior (leak scan, redaction, wrangler) stays on isSensitive.
  • Public+dynamic browser hydrationloadPublicDynamicEnv/setPublicDynamicEnv/getPublicDynamicEnv in runtime/env.ts, gated by a bundler-injected __varlockPublicDynamicKeys allow-list; Astro auto-injects /__varlock/public-env, plus a boot-time globalThis.__varlockPublicDynamicEnv pickup path.
  • Build-time access guard__VARLOCK_EXECUTION_PHASE=build (vite) makes the ENV proxy throw (or warn via _VARLOCK_DYNAMIC_BUILD_ACCESS_MODE) on public+dynamic access during build/prerender; Next marks routes dynamic via a headers() access hook.
  • Metadata-first scoped filter resolution — new @dynamic filter selector; resolveEnvValuesForFilter resolves decorator-selector filters against item metadata only, so excluded items' value resolvers never run.
  • Docs + framework-tests — new Static vs Dynamic guide, reference updates, and Next/Astro/SvelteKit/Vite/expo scenarios.

I traced the secret-safety story across every changed inlining/hydration site: the blob-serialization divergence check, the per-integration gates, and the browser hydration allow-list all correctly apply isDynamic ?? isSensitive (and && !isSensitive for the public-dynamic subset), so no @sensitive value can reach a client bundle through the changed paths. The @sensitive @static inlining in getBuildTimeReplacements is an intentional, leak-scanner-backstopped opt-in. The scoped-filter preEvaluate/matches/metadataDependencyKeys logic is sound. One narrow staleness gap is noted inline.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using Claude Opus𝕏

Comment thread packages/varlock/src/env-graph/lib/config-item.ts Outdated
@theoephraim

Copy link
Copy Markdown
Member Author

@pullfrog - review again, we changed the model

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The boot-time injection recipe embeds valid JSON in an unsafe HTML script context, which can enable script injection.

Reviewed changes: The head is unchanged since the prior Pullfrog review; this pass independently re-reviewed the full PR using the updated model.

  • Revalidated static and dynamic config semantics: Traced decorator resolution, serialized graph defaults, scoped filters, and integration inlining gates.
  • Rechecked public dynamic delivery: Traced endpoint hydration, browser allow-listing, build-time access guards, and the boot-time HTML injection recipe.
  • Rechecked framework behavior: Covered the Astro, Next.js, Vite, SvelteKit, Expo, and Cloudflare paths and their tests and documentation.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using azure/gpt-5.6-sol𝕏

Comment thread packages/varlock-website/src/content/docs/guides/dynamic-config.mdx
…pe <), compute isDynamic for proxy-view items

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes: the latest commit closes both prior findings without introducing a new regression.

  • Hardened JSON output for HTML embedding: varlock load --format json now escapes < as \u003C, preserving valid JSON and exact parsed values while preventing </script> from terminating the documented inline script.
  • Corrected proxy-view dynamic metadata: ConfigItem.resolveMetadata() now computes isDynamic for proxy-view items while continuing to skip resolver-implied sensitivity processing.
  • Added focused regression coverage: CLI serialization tests cover hostile and benign payloads, and proxy tests cover a sensitive static item under a proxy resolution view.

Pullfrog  | View workflow run | Using azure/gpt-5.6-sol𝕏

…time docker recipe (sed) so --format json output stays unchanged

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues. One minor documentation correction is noted inline.

Reviewed changes: the latest commit narrows HTML script escaping to the context that requires it while preserving the normal JSON CLI contract.

  • Restored standard JSON output: reverted global < escaping from varlock load --format json.
  • Localized script-context escaping: updated the boot-time injection recipe to transform < into \u003C before embedding the payload in HTML.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using azure/gpt-5.6-sol𝕏

set -e

# resolve only the public+dynamic subset from the container env, then escape `<`
# as `<` so a value containing `</script>` cannot break out of the tag

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment says < is escaped as <, but the command actually emits \u003C and the prose below relies on that distinction. Please name \u003C here so the copied recipe explains its safety step accurately.

@theoephraim
theoephraim merged commit 96bf043 into main Jul 28, 2026
38 checks passed
@theoephraim theoephraim mentioned this pull request Jul 28, 2026
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant